﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Web.Hosting;
using System.Xml;
using Jayden.Dll.Core.Persistence.Enums;
using Jayden.Dll.Core.Persistence.Models;

namespace Jayden.Dll.Core.Persistence.Xml
{
	public class XmlDataAccess<T> : DataAccess<T> where T : Object
	{
		private string m_XmlFilePath;
		private string m_RootXPath;
		private XmlElement m_RootElement;
		private Configuration m_Configuration;
		private UnitOfWork m_UnitOfWork;
		private Entity m_Entity;
		private XmlDocumentLock m_Lock;

		public XmlDataAccess(Configuration config, UnitOfWork uow, Entity entity)
		{
			m_Configuration = config;
			m_Entity = entity;
			m_UnitOfWork = uow;
			m_XmlFilePath = HostingEnvironment.MapPath(string.Format(config.GetValue("path", "/{0}.xml"), Entity.FullName));
			m_RootXPath = config.GetValue("xpath", string.Empty);
		}

		public UnitOfWork UnitOfWork { get { return m_UnitOfWork; } }
		public Entity Entity { get { return m_Entity; } }
		public Configuration Configuration { get { return m_Configuration; } }

		private XmlDocumentLock Lock
		{
			get
			{
				if (m_Lock == null)
					m_Lock = XmlDocumentLock.GetLock(m_XmlFilePath);
				return m_Lock;
			}
		}
		public XmlNodeProxy GetRoot(bool writeAccess)
		{
			return Lock.GetNode(m_RootXPath ,writeAccess);
		}

		protected long GetNextAutoIncrementedValue(Property p, XmlElement root)
		{
			lock (root)
			{
				if (p.PropertyInfo.PropertyType != typeof(long))
					throw new NotImplementedException();
				XmlAttribute attribute = root.Attributes[p.Field.FieldName];
				if (attribute == null)
				{
					attribute = root.OwnerDocument.CreateAttribute(p.Field.FieldName);
					attribute.Value = 1.ToString();
					root.Attributes.Append(attribute);
				}
				long value = long.Parse(attribute.Value);
				attribute.Value = (value + 1).ToString();
				return value;
			}
		}

		public void Create(T obj)
		{
			using (XmlNodeProxy root = GetRoot(true))
			{
				XmlDocument doc = root.Node.OwnerDocument;
				XmlElement entityElement = doc.CreateElement(Entity.Table.TableName);
				foreach (Property p in Entity.Properties)
				{
					if (p.Field.AutoIncrement)
						p.SetValue(obj, GetNextAutoIncrementedValue(p, root.Node as XmlElement));
					object value = p.GetValue(obj);
					if (value != null)
					{
						XmlAttribute attribute = doc.CreateAttribute(p.Field.FieldName);
						entityElement.Attributes.Append(attribute);
						attribute.Value = GetStringFromValue(p, value);
					}
				}
				root.Node.AppendChild(entityElement);
			}
		}

		public virtual string GetStringFromValue(Property property, object value)
		{
			if (value is IFormattable)
				return ((IFormattable)value).ToString(null, CultureInfo.InvariantCulture);
			return value.ToString();
		}

		public virtual object GetValueFromString(Property property, XmlAttribute attribute)
		{
			if (attribute == null)
				return property.Field.DefaultValue;
			if (property.PropertyInfo.PropertyType == typeof(string))
				return attribute.Value;
			if (property.PropertyInfo.PropertyType == typeof(ulong))
				return ulong.Parse(attribute.Value, CultureInfo.InvariantCulture);
			if (property.PropertyInfo.PropertyType == typeof(long))
				return long.Parse(attribute.Value, CultureInfo.InvariantCulture);
			if (property.PropertyInfo.PropertyType == typeof(uint))
				return uint.Parse(attribute.Value, CultureInfo.InvariantCulture);
			if (property.PropertyInfo.PropertyType == typeof(int))
				return int.Parse(attribute.Value, CultureInfo.InvariantCulture);
			if (property.PropertyInfo.PropertyType == typeof(DateTime))
				return DateTime.Parse(attribute.Value, CultureInfo.InvariantCulture);
			if (property.PropertyInfo.PropertyType == typeof(bool))
				return bool.Parse(attribute.Value);
			if (property.PropertyInfo.PropertyType.IsEnum)
				return Enum.Parse(property.PropertyInfo.PropertyType, attribute.Value);
			throw new Exception(string.Format("Unable to parse type '{0}' from string '{1}'.", property.PropertyInfo.PropertyType, attribute.Value));
		}


		protected virtual object GetPropertyValueFromElement(Property property, XmlElement elt)
		{
			XmlAttribute attr = elt.Attributes[property.Field.FieldName];
			if (attr == null)
				return null;
			return GetValueFromString(property, attr);
		}


		protected virtual T CreateFromXmlElement(XmlElement elt)
		{
			T obj = default(T);
			if (elt != null && elt.Name == Entity.Table.TableName)
			{
				ObjectId oid = UnitOfWork.NewObjectId(Entity);
				AssignObjectIdXmlElement(oid, elt);
				lock (UnitOfWork.RegistrationLock)
				{
					obj = UnitOfWork.GetRegistered<T>(oid);
					if (obj != null)
						return obj;
					obj = UnitOfWork.NewObject<T>(Entity, oid);
					foreach (Property p in Entity.Properties)
						p.SetValue(obj, GetValueFromString(p, elt.Attributes[p.Field.FieldName]));
					obj.State = ObjectState.Clean;
					UnitOfWork.Register(obj);
				}
			}
			return obj;
		}

		protected virtual void AssignObjectIdXmlElement(ObjectId oid, XmlElement elt)
		{
			for (int c = 0; c < oid.Lenght; c++)
			{
				XmlAttribute attribute = elt.Attributes[Entity.PrimaryKey[c].Field.FieldName];
				if (attribute != null)
					oid[c] = GetValueFromString(Entity.PrimaryKey[c], attribute);
			}
		}

		protected string XPathPrimaryKey(ObjectId id)
		{
			List<string> pks = new List<string>();
			for (int c = 0; c < id.Lenght; c++)
				pks.Add(string.Format("{0}=\"{1}\"", Entity.PrimaryKey[c].Field.FieldName, GetStringFromValue(Entity.PrimaryKey[c], id[c])));
			string xpathPk = string.Join(" and ", pks);
			return string.Format("{0}[@{1}]", Entity.Table.TableName, xpathPk);
		}

		public T Read(ObjectId id) { return ReadInternal(id); }
		public T ReadInternal(ObjectId id)
		{
			string xpath = XPathPrimaryKey(id);
			using (XmlNodeProxy root = GetRoot(false))
			{
				XmlNode node = root.Node.SelectSingleNode(xpath);
				if (node == null)
					return default(T);
				return CreateFromXmlElement(node as XmlElement);
			}
		}

		public void Update(T obj)
		{
			using (XmlNodeProxy root = GetRoot(true))
			{
				string xpath = XPathPrimaryKey(obj.ObjectId);
				XmlNode node = root.Node.SelectSingleNode(xpath);
				if (node != null)
				{
					foreach (Property p in Entity.Properties)
					{
						XmlAttribute attribute = node.Attributes[p.Field.FieldName];
						object value = p.GetValue(obj);
						if (attribute == null && value != null)
						{
							attribute = root.Node.OwnerDocument.CreateAttribute(p.Field.FieldName);
							node.Attributes.Append(attribute);
						}
						if (attribute != null && value == null)
							node.Attributes.Remove(attribute);
						else if(value != null)
							attribute.Value = GetStringFromValue(p, value);
					}
				}
			}
		}

		public void Create(Object obj) { Create((T)obj); }

		Object DataAccess.Read(ObjectId id) { return ReadInternal(id); }

		public void Update(Object obj) { Update((T)obj); }

		public void Delete(ObjectId id)
		{
			using (XmlNodeProxy root = GetRoot(true))
			{
				string xpath = XPathPrimaryKey(id);
				XmlNode node = root.Node.SelectSingleNode(xpath);
				if (node != null)
					node.ParentNode.RemoveChild(node);
			}
		}

		public System.Data.DataTable Read(Queries.Query query, Dictionary<string, object> parameters = null)
		{
			throw new NotImplementedException();
		}

		public void Dispose() { }


		public void BeginTransaction() { }

		public void CommitTransaction() { }

		public void RollbackTransaction() { }
	}
}
